'''
akshare股票数据，csv指标文件
止损:金叉后跌破买入价的%.

'''
import backtrader as bt  
import pandas as pd
from datetime import datetime
import matplotlib.pyplot as plt

from backtrader_plotting import Bokeh
from backtrader_plotting.schemes import Tradimo

import csv

import akshare as ak
# 获取上证指数的历史行情数据  
data_stock = ak.stock_zh_index_daily(symbol="sh000001")  

# ============================清理数据=================================================
# 读取csv
data_macd = pd.read_csv('法宝回测/data/bwwmacd.csv') # 股票指标数据(大智慧导出)
selected_columns = ['日期','MA1','MA2','MA3','S' ]
data_macd = data_macd[selected_columns]
# 改变列名称
data_macd.columns = ['date', 'M5', 'M10', 'M20','MA']


# 日期列转换时间类型
data_stock['date'] = pd.to_datetime(data_stock['date'])
data_macd['date'] = pd.to_datetime(data_macd['date'])

# 时间索引
data_stock.index = pd.to_datetime(data_stock['date'])
data_macd.index = pd.to_datetime(data_macd['date'])

# 截取数据
# 定义开始和结束日期  
start_date = '2023-05-01'  # 例如：2023年1月1日  
end_date = '2024-06-14'   # 例如：2023年6月30日  

# 将字符串转换为datetime对象（如果它们不是的话）  
start_date = datetime.strptime(start_date, '%Y-%m-%d')  
end_date = datetime.strptime(end_date, '%Y-%m-%d') 

mask = (data_stock.date >= start_date) & (data_stock.date <= end_date)  
data_stock = data_stock.loc[mask] 
data_macd=data_macd.loc[mask] 

# ========================自定义数据源类=======================================================
# 这两个类都继承自 bt.feeds.PandasData，意味着它们能够处理Pandas DataFrame格式的数据。
# 在策略中，您可以通过 self.datas[0] 访问股票数据，通过 self.datas[1] 访问信号数据。

# 自定义数据源，这里仅展示框架，具体实现需要根据你的CSV文件格式进行调整  
class MyStockData(bt.feeds.PandasData):  # MyStockData 类定义了如何从Pandas DataFrame中提取股票数据。
    # params 元组指定了日期、开盘价、最高价、最低价、收盘价和交易量（如果有的话）在DataFrame中的列索引。
    params = (  
        ('datetime', 0),  # 日期在DataFrame中的列索引 
        ('high', 1),      # 最高价在DataFrame中的列索引 
        ('low', 2),       # 最低价在DataFrame中的列索引  
        ('open', 3),      # 开盘价在DataFrame中的列索引  
        ('close', 4),     # 收盘价在DataFrame中的列索引  
        ('volume', 5),    # 交易量在DataFrame中的列索引（如果有的话）  
    )  
class MySignalData(bt.feeds.PandasData):  # MySignalData 类用于处理信号数据。
    lines = ('M5','M10','M20','MA',)  # lines = ('signal',) 是一个元组的示例，
    # 指定了日期和信号在DataFrame中的列索引。
    params = (  
        ('datetime', 0),  # 日期在DataFrame中的列索引
        ('M5', 1), # 假设'M5'是某种信号，这里重命名以避免混淆  
        ('M10', 2), # 同上  
        ('M20', 3), # 同上 
        ('MA', 4), # 同上 
    ) 
    
#===========策略策略策略策略策略策略策略策略策略策略策略======================================
class MyStrategy(bt.Strategy):
    params = (  
        ('printlog', True),  
        #('stop_loss', 0.03),  # 止损比例，比如3%
        ("ma", 5), 
        ("fast_period", 1),  # 短期（5日）移动平均线周期
        ("slow_period", 5),  # 长期（30日）移动平均线周期
        
        ("macd_fast", 12),  # MACD快线周期
        ("macd_slow", 26),  # MACD慢线周期
        ("macd_signal", 9),  # MACD信号线周期
    )

    def __init__(self):  
        # 初始化策略  
        self.dataclose = self.datas[0].close  
        
        # 记录买入价格
        self.buy_price = None
        
        sma5 = bt.ind.SMA(period=self.p.ma)  # fast moving average
        # 初始化短期和长期移动均线指标
        self.fast_sma = bt.indicators.SimpleMovingAverage(self.datas[1].M5, period=self.params.fast_period)
        self.slow_sma = bt.indicators.SimpleMovingAverage(self.datas[1].M5, period=self.params.slow_period)
        self.crossover = bt.ind.CrossOver(self.fast_sma, self.slow_sma)  # crossover signal
        
        # 初始化MACD指标
        self.macd = bt.indicators.MACDHisto(self.datas[0],
                                           period_me1=self.params.macd_fast,
                                           period_me2=self.params.macd_slow,
                                           period_signal=self.params.macd_signal)
        # 检查MACD金叉和死叉
        self.macd_line = self.macd.lines.macd  # MACD线（快线-慢线）
        self.signal_line = self.macd.lines.signal  # 信号线
        self.buysize = 0
        self.stop_loss=0
        

    def downcast(self, amount, lot): #可买入资金
	    #使用整数除法 amount // lot 来找到 amount 可以被 lot 整除的最大次数。
        return abs(amount // lot * lot) 
        """ 	   
		self 的出现意味着这个函数实际上是类的一个方法，而不是一个独立的函数。
		class MyClass:  
 		   def downcast(self, amount, lot):  
  		      return abs(amount // lot * lot)  

		# 使用方法  
		my_instance = MyClass()  
		result = my_instance.downcast(123, 10)  
		print(result)  # 输出 120
	    """
    # 可以不要，但如果你数据未对齐，需要在这里检验
    # def prenext(self):
    #     print('prenext 执行 ', self.datetime.date(), self.getdatabyname('300015')._name
    #           , self.getdatabyname('300015').close[0])
    #     pass
    
    #=================================NEXT===========================================
    def next(self):
        
        # 计算止损价格
        # macd零上零下区别对待
        if self.macd_line < 0:
            self.stop_loss=0.01
        else:
            self.stop_loss=0.03
        stop_price = self.buy_price * (1 - self.stop_loss) if self.buy_price else 0
        
        # if self.order:
        #     return
        # 回测如果是最后一天，则不进行买卖
        if pd.Timestamp(self.data.datetime.date(0)) == end_date:
            return
        if not self.position:  # 没有持仓
            # 执行买入条件判断：收盘价格上涨突破20日均线；
            # 不要在股票剔除日前一天进行买入
            if self.crossover > 0 and self.fast_sma <-500: # or self.fast_sma > 0 ):# and self.data1M5 < self.data1MA:  # 如果信号为1，买入  
                if self.macd_line < 0:
                    # 更新买入价格
                    self.buy_price = self.data.close[0]
                    print('执行半仓买入') 
                    order_value = self.broker.getvalue() * 0.98
                    order_amount = self.downcast(order_value / self.data.close[0], 100)
                    #半仓
                    order_amount=order_amount/2
                    self.order = self.buy(self.datas[0], order_amount, name=self.datas[0]._name)
                elif self.macd_line > 0:
                    self.buy_price = self.data.close[0]
                    print('执行全仓买入') 
                    order_value = self.broker.getvalue() * 0.98
                    order_amount = self.downcast(order_value / self.data.close[0], 100)
                    #全仓
                    
                    self.order = self.buy(self.datas[0], order_amount, name=self.datas[0]._name)
                    
        
        else:
            # 执行卖出条件判断
            # 如果当前价格低于止损价格，则卖出
            # if self.data.close[0] < stop_price:
            #     #print(f"止损卖出 { self.data.datetime.date(0)}, {stop_price},{self.data0.close[0]}")
                
            #     self.close()
                    
            if self.crossover < 0 :
                # 执行卖出
                print('执行卖出') 
                self.order = self.order_target_percent(self.datas[0], 0, name=self.datas[0]._name)
                self.log(f'卖{self.datas[0]._name},price:{self.data.close[0]:.2f},pct: 0')
            elif self.datas[1].lines.M5[0] > self.datas[1].lines.M10[0] and self.data.close[0] < stop_price: 
                print('止损')   
                self.order = self.order_target_percent(self.datas[0], 0, name=self.datas[0]._name)
                self.log(f'卖{self.datas[0]._name},price:{self.data.close[0]:.2f},pct: 0')
        pass
    # ==================打印信息============================================
    def notify_order(self, order): # 用于通知和处理订单信息。
        # 检查order.status是否属于两个可能的状态之一：order.Submitted 和 order.Accepted。
        if order.status in [order.Submitted, order.Accepted]:
            # Buy/Sell order submitted/accepted to/by broker - Nothing to do
            return

        # Check if an order has been completed
        # Attention: broker could reject order if not enough cash
        if order.status in [order.Completed, order.Canceled, order.Margin]:
            if order.isbuy():
                self.log(
                    f"买入{order.info['name']}, 成交量{order.executed.size}，成交价{order.executed.price:.2f} 订单状态：{order.status}")
                self.log('买入后当前资产：%.2f 元' % self.broker.getvalue())
            elif order.issell():
                self.log(
                    f"卖出{order.info['name']}, 成交量{order.executed.size}，成交价{order.executed.price:.2f} 订单状态：{order.status}")
                self.log('卖出后当前资产：%.2f 元' % self.broker.getvalue())
            self.bar_executed = len(self)

        # Write down: no pending order
        self.order = None

    def log(self, txt, dt=None): #日志-日期和信息
        """
        输出日期
        :param txt:
        :param dt:
        :return:
        """
        dt = dt or self.datetime.date(0)  # 现在的日期
        #'%s , %s' 是一个字符串模板，其中%s是一个占位符，用于表示一个字符串。
        # 在这个模板中，有两个%s，这意味着我们需要为它们提供两个字符串值。
        # (dt.isoformat(), txt) 是一个元组，它包含了两个值：dt.isoformat() 和 txt。
        # isoformat方法默认会生成一个包含T的ISO 8601格式的字符串（如2023-10-23T14:30:00），
        # 所以在输出的日期和时间之间会有一个T
        print('%s , %s' % (dt.isoformat(), txt))

    pass

    def notify_trade(self, trade): #打印交易信息
        '''可选，打印交易信息'''
        pass
    
#============== 创建Cerebro引擎========================================
# 创建Cerebro引擎  
cerebro = bt.Cerebro()  

# 设置回测时间范围
start_date = datetime(2023, 5, 1)
end_date = datetime(2024, 6, 27)
  

# 添加数据源  
datafeed_stock = MyStockData(dataname=data_stock, fromdate=start_date, todate=end_date)  
datafeed_signal = MySignalData(dataname=data_macd, fromdate=start_date, todate=end_date)  

# 将数据源添加到cerebro ，一定要加name=......, 否则出不了图
cerebro.adddata(datafeed_stock, name="000001")  
cerebro.adddata(datafeed_signal, name="bwwmacd")  

  
# 添加策略  
cerebro.addstrategy(MyStrategy)  
  
# 设置初始资金和手续费
start_cash = 1000000
cerebro.broker.setcash(start_cash)
cerebro.broker.setcommission(commission=0.002)

# 添加策略分析指标
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='tradeanalyzer')  
cerebro.addanalyzer(bt.analyzers.AnnualReturn, _name='annualReturn')  
cerebro.addanalyzer(bt.analyzers.Returns, _name='annualizedReturns', tann=252)  # 使用 'annualizedReturns' 代替 '_Returns'  
cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')  
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpeRatio')  # 使用 'sharpeRatio' 代替 'sharpe'  
cerebro.addanalyzer(bt.analyzers.Returns, _name='totalReturns')  # 使用 'totalReturns' 代替重复的 'returns'  
# 假设 'TimeReturn' 是有效的分析器  
cerebro.addanalyzer(bt.analyzers.TimeReturn, _name='timeReturn')  # 假设这是有效的

#cerebro.addanalyzer(bt.analyzers.PyFolio, _name='pyfolio')


# 运行回测
results = cerebro.run()

#=====================================#获取回测结果并打印========================
port_value = cerebro.broker.getvalue()
pnl = port_value - start_cash
print(f"初始资金: {start_cash}\n回测期间：{start_date.strftime('%Y%m%d')}:{end_date.strftime('%Y%m%d')}")
print(f"总资金: {round(port_value, 2)}")
print(f"净收益: {round(pnl, 2)}")#计算胜率
total_trades = results[0].analyzers.tradeanalyzer.get_analysis()['total']['total']
won_trades = results[0].analyzers.tradeanalyzer.get_analysis()['won']['total']
win_rate = (won_trades / total_trades) * 100 if total_trades > 0 else 0
print('总交易次数:', total_trades)
print('盈利次数:', won_trades)
print('胜率%:', win_rate)
# 打印分析器输出结果
# print(f"初始资金: {start_cash}\n回测期间:{start_date.strftime('%Y-%m-%d')} : {end_date.strftime('%Y-%m-%d')}")
# print('年度汇报:', results[0].analyzers.annualReturn.get_analysis())
print('年化收益%:', results[0].analyzers.annualizedReturns.get_analysis()['rnorm100'])
print('最大回撤比例%:', results[0].analyzers.drawdown.get_analysis().max.drawdown)
print('夏普比率:', results[0].analyzers.sharpeRatio.get_analysis()['sharperatio'])
print('累计收益：', results[0].analyzers.totalReturns.get_analysis()['rtot'])

print(f'最后投资金额：{round(cerebro.broker.getvalue(), 2)}')

#====================================出图==============================================
plotconfig = {
    'id:ind#0': dict(
        subplot=True,
    ),
}
b = Bokeh(style='line', scheme=Tradimo(),plotconfig=plotconfig)
cerebro.plot(b)